﻿// reference http://www.codeproject.com/KB/aspnet/test-with-vs-devserver-2.aspx
// Date: Oct 02 2010
// Author: Sky Sanders (http://skysanders.net/subtext)
// License: public domain - attribution and link love appreciated

// If Visual Studio development server serves your needs capably, this is the leanest
// self hosting server around.

// For a more robust development server see http://cassinidev.codeplex.com

#region

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Management;
using System.Net;
using System.Net.NetworkInformation;
using System.Text.RegularExpressions;
using System.Threading;

#endregion

namespace Salient.Excerpts
{
    /// <summary>
    /// A general purpose Visual Studio 2008 Development Server 
    /// (WebDev.WebServer.exe) test fixture.
    /// Controls a shelled instance of WebDev.WebServer.exe 
    /// so F5 debugging is not possible with this server.
    /// </summary>
    /// NOTE: code from various namespaces/classes in the 
    /// Salient project have been merged into this
    /// single class for this post in the interest of brevity.
    public class WebDevServer
    {
        public WebDevServer()
        {
        }

        public string ApplicationPath { get; private set; }
        public string HostName { get; private set; }
        public int Port { get; private set; }
        public string VirtualPath { get; private set; }

        public string RootUrl
        {
            get { return string.Format(CultureInfo.InvariantCulture, 
        "http://{0}:{1}{2}", HostName, Port, VirtualPath); }
        }

        private int ProcessId { get; set; }

        private static string ProgramFilesx86
        {
            get
            {
                if (8 == IntPtr.Size ||
                    (!String.IsNullOrEmpty(Environment.GetEnvironmentVariable
            ("PROCESSOR_ARCHITEW6432"))))
                {
                    return Environment.GetEnvironmentVariable("ProgramFiles(x86)");
                }

                return Environment.GetEnvironmentVariable("ProgramFiles");
            }
        }

        /// <summary>
        /// Combine the RootUrl of the running web application with the 
        /// relative URL specified.
        /// </summary>
        public virtual Uri NormalizeUri(string relativeUrl)
        {
            return new Uri(RootUrl + relativeUrl);
        }

        /// <summary>
        /// Will start "localhost" on first available port in the range 
        /// 8000-10000 with vpath "/"
        /// </summary>
        /// <param name="applicationPath"></param>
        public void StartServer(string applicationPath)
        {
            StartServer(applicationPath, GetAvailablePort
        (8000, 10000, IPAddress.Loopback, true), "/", "localhost");
        }

        /// <summary>
        /// </summary>
        /// <param name="applicationPath">Physical path to application.</param>
        /// <param name="port">Port to listen on.</param>
        /// <param name="virtualPath">Optional. defaults to "/"</param>
        /// <param name="hostName">Optional. 
        /// Is used to construct RootUrl. Defaults to "localhost"</param>
        public void StartServer(string applicationPath, int port, 
        string virtualPath, string hostName)
        {
            applicationPath = Path.GetFullPath(applicationPath);
            hostName = string.IsNullOrEmpty(hostName) ? "localhost" : hostName;
            virtualPath = String.Format("/{0}/", 
        (virtualPath ?? string.Empty).Trim('/')).Replace("//", "/");

            if (GetRunningInstance(applicationPath, port, virtualPath) != null)
            {
                return;
            }

            IPAddress ipAddress = IPAddress.Loopback;

            if (!IsPortAvailable(ipAddress, port))
            {
                throw new Exception(string.Format("Port {0} is in use.", port));
            }

            string arguments = String.Format
        (CultureInfo.InvariantCulture, "/port:{0} /path:\"{1}\" /vpath:\"{2}\"",
                                             port, applicationPath, virtualPath);

            using (Process proc = new Process())
            {
                proc.StartInfo = new ProcessStartInfo
                    {
                        FileName = GetWebDevExecutablePath(),
                        Arguments = arguments,
                        CreateNoWindow = true
                    };
                bool started = proc.Start();
                if (!started)
                {
                    throw new Exception("Error starting server");
                }
                ProcessId = proc.Id;
            }

            ApplicationPath = applicationPath;
            Port = port;
            VirtualPath = virtualPath;
            HostName = hostName;
        }

        /// <summary>
        /// After stopping an instance of WebDevWebServer.exe, 
        /// the orphaned tray notification icon will remain.
        /// This can be annoying but simply running your mouse over them 
        /// will dispose of it.
        /// I think there may be some Win32 voodoo to deal with this 
        /// but is a very low priority right now.
        /// </summary>
        public void StopServer()
        {
            try
            {
                WebDevServer instance = GetRunningInstance
            (ApplicationPath, Port, VirtualPath);
                if (instance != null)
                {
                    using (Process process = Process.GetProcessById(instance.ProcessId))
                    {
                        if (process.MainWindowHandle != IntPtr.Zero)
                        {
                            process.CloseMainWindow();
                        }
                        process.Kill();

                        // allow the port time to be released
                        process.WaitForExit(100);
                    }
                }
            }

                // ReSharper disable EmptyGeneralCatchClause
            catch
            // ReSharper restore EmptyGeneralCatchClause
            {
                // what can we do about spurious exceptions on Process.Kill?
            }
        }

        public void Dispose()
        {
            StopServer();
        }

        #region Instance Management


#if NET2FX2
        private const string WebDevPath = 
        @"Common Files\Microsoft Shared\DevServer\9.0\WebDev.WebServer.exe";
#endif

#if NET2FX4
        private const string WebDevPath = 
        @"Common Files\Microsoft Shared\DevServer\10.0\WebDev.WebServer20.exe";
#endif


#if NET4FX4
        private const string WebDevPath =
        @"Common Files\Microsoft Shared\DevServer\10.0\WebDev.WebServer40.exe";
#endif


        private static readonly Regex RxPath = 
        new Regex(@"/path:""(?<path>[^""]*)", RegexOptions.ExplicitCapture);
        private static readonly Regex RxPort = 
        new Regex(@"/port:(?<port>\d*)", RegexOptions.ExplicitCapture);
        private static readonly Regex RxVPath = 
        new Regex(@"/vpath:""(?<vpath>[^""]*)", RegexOptions.ExplicitCapture);

        /// <summary>
        /// Private ctor to build from WMI query
        /// </summary>
        private WebDevServer(string commandLine, int processId)
        {
            ProcessId = processId;
            Port = Int32.Parse(RxPort.Match(commandLine).Groups["port"].Value);
            ApplicationPath = RxPath.Match(commandLine).Groups["path"].Value;
            VirtualPath =
                String.Format("/{0}/", RxVPath.Match(commandLine).Groups
            ["vpath"].Value.Trim('/')).Replace("//", "/");
            HostName = "localhost";
        }

        public static WebDevServer GetRunningInstance(string applicationPath, 
                                   int port, string virtualPath)
        {
            return GetRunningInstances().FirstOrDefault(s =>
                string.Compare(s.ApplicationPath, applicationPath, 
            StringComparison.OrdinalIgnoreCase) == 0 &&
                string.Compare(s.VirtualPath, virtualPath, 
            StringComparison.OrdinalIgnoreCase) == 0 &&
                s.Port == port);
        }

        /// <summary>
        /// Queries WMI and builds a list of WebDevWebServer 
        /// representing all running instances.
        /// </summary>
        public static List<WebDevServer> GetRunningInstances()
        {
            List<WebDevServer> returnValue = new List<WebDevServer>();
            const string query = "select CommandLine,ProcessId from Win32_Process where Name='WebDev.WebServer.EXE'";
            using (ManagementObjectSearcher searcher = 
                new ManagementObjectSearcher(query))
            {
                using (ManagementObjectCollection results = searcher.Get())
                {
                    foreach (ManagementObject process in results)
                    {
                        returnValue.Add(
                            new WebDevServer(process["CommandLine"].ToString(), 
                                int.Parse(process["ProcessId"].ToString())));
                        process.Dispose();
                    }
                }
            }
            return returnValue;
        }

        public static string GetWebDevExecutablePath()
        {
            string exePath = Path.Combine(ProgramFilesx86, WebDevPath);

            if (!File.Exists(exePath))
            {
                throw new FileNotFoundException(exePath);
            }
            return exePath;
        }

        /// <summary>
        /// Gently polls specified IP:Port to determine if it is available.
        /// </summary>
        /// <param name="ipAddress"></param>
        /// <param name="port"></param>
        public static bool IsPortAvailable(IPAddress ipAddress, int port)
        {
            bool portAvailable = false;

            for (int i = 0; i < 5; i++)
            {
                portAvailable = GetAvailablePort(port, port, ipAddress, true) == port;
                if (portAvailable)
                {
                    break;
                }
                // be a little patient and wait for the port if necessary,
                // the previous occupant may have just vacated
                Thread.Sleep(100);
            }
            return portAvailable;
        }

        /// <summary>
        /// Returns first available port on the specified IP address.
        /// The port scan excludes ports that are open on ANY loopback adapter.
        ///
        /// If the address upon which a port is requested is an 'ANY' address all
        /// ports that are open on ANY IP are excluded.
        /// </summary>
        /// <param name="rangeStart"></param>
        /// <param name="rangeEnd"></param>
        /// <param name="ip">The IP address upon which to search for available port.
        /// </param>
        /// <param name="includeIdlePorts">If true includes ports in 
        /// TIME_WAIT state in results.
        /// TIME_WAIT state is typically cool down period for recently released ports.
        /// </param>
        /// <returns></returns>
        public static int GetAvailablePort(int rangeStart, 
               int rangeEnd, IPAddress ip, bool includeIdlePorts)
        {
            IPGlobalProperties ipProps = IPGlobalProperties.GetIPGlobalProperties();

            // if the ip we want a port on is an 'any' or loopback port 
            // we need to exclude all ports that are active on any IP
            Func<IPAddress, bool> isIpAnyOrLoopBack = i => IPAddress.Any.Equals(i) ||
                                                           IPAddress.IPv6Any.Equals(i) ||
                                                           IPAddress.Loopback.Equals(i) ||
                                                           IPAddress.IPv6Loopback.
                                                               Equals(i);
            // get all active ports on specified IP.
            List<ushort> excludedPorts = new List<ushort>();

            // if a port is open on an 'any' or 'loopback' interface 
            // then include it in the excludedPorts
            excludedPorts.AddRange(from n in ipProps.GetActiveTcpConnections()
                                   where
                                       n.LocalEndPoint.Port >= rangeStart &&
                                       n.LocalEndPoint.Port <= rangeEnd && (
                                       isIpAnyOrLoopBack(ip) || 
                    n.LocalEndPoint.Address.Equals(ip) ||
                                        isIpAnyOrLoopBack(n.LocalEndPoint.Address)) &&
                                        (!includeIdlePorts || n.State 
                    != TcpState.TimeWait)
                                   select (ushort)n.LocalEndPoint.Port);

            excludedPorts.AddRange(from n in ipProps.GetActiveTcpListeners()
                                   where n.Port >= rangeStart && n.Port <= rangeEnd && (
                                   isIpAnyOrLoopBack(ip) || 
                n.Address.Equals(ip) || isIpAnyOrLoopBack(n.Address))
                                   select (ushort)n.Port);

            excludedPorts.AddRange(from n in ipProps.GetActiveUdpListeners()
                                   where n.Port >= rangeStart && n.Port <= rangeEnd && (
                                   isIpAnyOrLoopBack(ip) || 
                n.Address.Equals(ip) || isIpAnyOrLoopBack(n.Address))
                                   select (ushort)n.Port);

            excludedPorts.Sort();

            for (int port = rangeStart; port <= rangeEnd; port++)
            {
                if (!excludedPorts.Contains((ushort)port))
                {
                    return port;
                }
            }

            return 0;
        }

        #endregion
    }
}